﻿// =============================================================================
// TNTTools SimpleSerialization.cs
// Copyright (c) 2013 TNTTechnologies Inc.
// =============================================================================

#region

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;
using System.Text;

using TNTTools.Reflection;
using TNTTools.Reflection.Interfaces;
using TNTTools.Serialization.Interfaces;
using TNTTools.XML;

#endregion

namespace TNTTools.Serialization{
    public class SimpleSerialization : IFormatter, ISimpleSerializer, IBasicTypes{
        private const int StringBuilderDefaultCapacity = 64000;
        protected List<Type> basicTypes = null;
        private SerializationBinder binder;
        private StreamingContext context;
        private object criticalSection = new object();
        private Type parentDeserializeType = null;
        private IReflectionPropertyCache reflectionCache;
        private ISurrogateSelector surrogateSelector;

        public SimpleSerialization(IReflectionPropertyCache reflectionCache){
            this.reflectionCache = reflectionCache;
            this.context = new StreamingContext(StreamingContextStates.All);
            this.basicTypes = this.GetAttributeTypes();
        }

        #region IBasicTypes Members

        public List<Type> GetBasicTypes(){
            return this.GetAttributeTypes();
        }

        #endregion

        #region IFormatter Members

        public ISurrogateSelector SurrogateSelector{
            get{
                return this.surrogateSelector;
            }
            set{
                this.surrogateSelector = value;
            }
        }

        public SerializationBinder Binder{
            get{
                return this.binder;
            }
            set{
                this.binder = value;
            }
        }

        public StreamingContext Context{
            get{
                return this.context;
            }
            set{
                this.context = value;
            }
        }

        /// <summary>
        ///     Use the facade layer instead
        ///     TODO: implement cached binding.
        /// </summary>
        /// <param name="serializationStream"></param>
        /// <returns></returns>
        public object Deserialize(Stream serializationStream){
            // See comments on overloads as to why this is.
            throw new NotImplementedException(" Use facade layer instead");
        }

        public void Serialize(Stream serializationStream, object source){
            this.Serialize(serializationStream, source, StringBuilderDefaultCapacity);
        }

        #endregion

        #region ISimpleSerializer Members

        public void Serialize(StringBuilder stringStream, object source){
            Type sourceType = source.GetType();
            ObjectInfo objectInfo = this.reflectionCache.GetCachedObjectInfo(source, sourceType, this, this);
            this.WriteElementStart(stringStream, sourceType, objectInfo);
            this.WriteObjectPropertiesToBuilder(stringStream, source, objectInfo);
            this.WriteElementEnd(stringStream, sourceType, objectInfo);
        }

        /// <summary>
        ///     define types we want as attributes
        /// </summary>
        public virtual List<Type> GetAttributeTypes(){
            if (this.basicTypes != null){
                return this.basicTypes;
            }
            this.basicTypes = new List<Type>();
            this.basicTypes.Add(typeof(Boolean));
            this.basicTypes.Add(typeof(Byte));
            this.basicTypes.Add(typeof(SByte));
            this.basicTypes.Add(typeof(Char));
            this.basicTypes.Add(typeof(Decimal));
            this.basicTypes.Add(typeof(Double));
            this.basicTypes.Add(typeof(Single));
            this.basicTypes.Add(typeof(Int32));
            this.basicTypes.Add(typeof(UInt32));
            this.basicTypes.Add(typeof(Int64));
            this.basicTypes.Add(typeof(UInt64));
            this.basicTypes.Add(typeof(Int16));
            this.basicTypes.Add(typeof(UInt16));
            this.basicTypes.Add(typeof(String));
            this.basicTypes.Add(typeof(DateTime));
            return this.basicTypes;
        }

        public virtual void WriteElementStart(StringBuilder stringStream, Type sourceType, ObjectInfo objectInfo){
            // Note: This is slightly faster than appendFormat
            stringStream.Append('<');
            stringStream.Append(sourceType.Name);
        }

        public virtual void WriteElementEnd(StringBuilder stringStream, Type sourceType, ObjectInfo objectInfo){
            // Note: This is slightly faster than appendFormat
            stringStream.Append('<');
            stringStream.Append('/');
            stringStream.Append(sourceType.Name);
            stringStream.Append('>');
        }

        public virtual void WriteXmlAttribute(
            StringBuilder stringStream, ReflectiveProperty infoEx, object value, string attributeName){
            stringStream.Append(' ');
            stringStream.Append(attributeName);
            stringStream.Append('=');
            stringStream.Append('\"');
            stringStream.Append(value);
            stringStream.Append('\"');
            stringStream.Append(' ');

            if (infoEx.IsLastBasicTypeAttribute){
                stringStream.Append('>');
                stringStream.Append(' ');
            }
            //Slightly slower
            //stringStream.AppendFormat(" {0}=\"{1}\" ", xmlAttributeValue, propValue);
        }

        #endregion

        public void Serialize(Stream serializationStream, object source, int initialCapacity){
            StringBuilder builder = new StringBuilder();
            builder.Capacity = initialCapacity;

            Serialize(builder, source);
            UnicodeEncoding uniEncoding = new UnicodeEncoding();
            StreamWriter sw = new StreamWriter(serializationStream);
            sw.Write(builder.ToString());
            sw.Flush();
        }

        public object Deserialize(Stream serializationStream, Type type){
            this.parentDeserializeType = type;
            StreamReader sr = new StreamReader(serializationStream);

            // Get Type from serialized data.
            XmlElement heirarchyElements = XmlHelpers.GetElements(sr.ReadToEnd());

            return this.Deserialize(serializationStream, heirarchyElements, type);
        }

        /// <summary>
        ///     Note: Does not support Binders but passing in the type is quicker anyway.
        ///     TODO: Refactor and push concerns to reflective property to remove some nested loops here.
        ///     TODO: Optimizations
        /// </summary>
        public object Deserialize(Stream serializationStream, XmlElement element, Type type){
            Object obj = FormatterServices.GetUninitializedObject(type);
            ReflectionPropertyCache cache = new ReflectionPropertyCache();
            ObjectInfo objectInfo = cache.GetCachedObjectInfo(obj, type, this, this);

            for (int i = 0; i < objectInfo.propertyInfo.Count; ++i){
                if (objectInfo.propertyInfo[i].isList){
                    Object objList = Activator.CreateInstance(objectInfo.propertyInfo[i].info.PropertyType);
                    // This following line fails to instantiate the object fully, by not intializing m_items in List
                    //Object objList = FormatterServices.GetUninitializedObject(objectInfo.propertyInfo[i].info.PropertyType);
                    Type listTemplateArgumentType =
                        objectInfo.propertyInfo[i].info.PropertyType.GetGenericArguments()[0]; //TODO: make more generic

                    for (int j = 0; j < element.ChildElements.Count; ++j) //TODO: Optimize ( use dictionary to find element by name)
                    {
                        if (objectInfo.propertyInfo[i].info.Name == element.ChildElements[j].Name) // TODO: avoid string compare
                        {
                            IList list = null;
                            for (int k = 0; k < element.ChildElements[j].ChildElements.Count; ++k){
                                object value = this.Deserialize(
                                    serializationStream,
                                    element.ChildElements[j].ChildElements[k],
                                    listTemplateArgumentType);
                                list = ((IList)objList);
                                list.Add(value);
                            }
                            if (list != null){
                                objectInfo.propertyInfo[i].setter.SetValue(objectInfo.propertyInfo[i], obj, list);
                            }
                        }
                    }
                } else{
                    bool isBasicType = (this.GetAttributeTypes().Contains(objectInfo.propertyInfo[i].info.PropertyType));
                    if (isBasicType || objectInfo.propertyInfo[i].getter.IsNullable) // TODO: Move isNullable to parent
                    {
                        this.SetProperty(obj, objectInfo.propertyInfo[i], element.Attributes);
                    } else{
                        for (int j = 0; j < element.ChildElements.Count; ++j) //TODO: Optimize
                        {
                            if (element.ChildElements[j].Name == objectInfo.propertyInfo[i].info.PropertyType.Name){
                                object value = this.Deserialize(
                                    serializationStream,
                                    element.ChildElements[j],
                                    objectInfo.propertyInfo[i].info.PropertyType);
                                objectInfo.propertyInfo[i].setter.SetValue(objectInfo.propertyInfo[i], obj, value);
                            }
                        }
                    }
                }
            }
            return obj;
        }

        /// <summary>
        ///     Find the Attribute that matches the property name and set it's value
        ///     TODO: Perhaps, refactor and move to property class
        /// </summary>
        private void SetProperty(object objectTarget, ReflectiveProperty reflectiveInfo, XmlAttributes attributes){
            string attributeValue = attributes[reflectiveInfo.info.Name]; // quick dictionary lookup
            if (attributeValue != null){
                reflectiveInfo.setter.SetValue(reflectiveInfo.type, objectTarget, attributeValue);
            }
        }

        /// <summary>
        ///     Loop through all the properties and write XML
        /// </summary>
        protected virtual void WriteObjectPropertiesToBuilder(
            StringBuilder stringStream, object source, ObjectInfo objectInfo){
            int propCount = objectInfo.propertyInfo.Count;

            for (int i = 0; i < propCount; ++i){
                objectInfo.propertyInfo[i].Write(stringStream, source, this);
            }
        }
    }
}